//  Inside Microsoft Exchange 2007 Web Services
//  PushNotificationClient.cs
//
//  Copyright (c) 2007 Grandma Lois who doesn't exist and also David Sterling, Ben Spain, Mark Taylor, 
//  Michael Mainer and Huw Upshall who do exist.
//  Listing 17-21
//

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Threading;
using System.Xml;
using System.IO;
using ProxyHelpers.EWS;
using System.Xml.Serialization;

namespace PushNotificationClient
{
	/// <summary>
	/// Enumeration indicating possible response values
	/// </summary>
	public enum NotificationResponse 
	{
		OK,
		Unsubscribe,
	}

	/// <summary>
	/// Delegate to be called when notifications are received
	/// </summary>
	/// <param name="sender">Sender (PushNotificationClient in this case)</param>
	/// <param name="notification">Actual Notification received from server</param>
	/// <returns>Response to send back to server</returns>
	/// 
	public delegate NotificationResponse NotificationEventsReceived(
								object sender, 
								SendNotificationResponseMessageType notification);

	/// <summary>
	/// Exception thrown when client is in wrong state for method call.
	/// </summary>
	public class PushNotificationStateException : Exception 
	{
		public PushNotificationStateException(string message) : base(message) { }
	}
	
	/// <summary>
	/// Client implementation
	/// </summary>
	public class PushNotificationClient
	{
		private uint portNumber;
		private NotificationEventsReceived eventHandler;
		private bool isListening = false;
		private ManualResetEvent stopEvent = new ManualResetEvent(false);
		private bool shouldStop = false;
		private XmlNamespaceManager mgr;
		private XmlSerializer ser;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="portNumber">Port number to listen on</param>
		/// <param name="eventHandler">delegate to call when notifications are received</param>
		/// 
		public PushNotificationClient(uint portNumber, NotificationEventsReceived eventHandler)
		{
			this.portNumber = portNumber;
			if (eventHandler == null)
			{
				throw new ArgumentNullException("eventHandler");
			}
			this.eventHandler = eventHandler;
			this.mgr = new XmlNamespaceManager(new NameTable());
			this.mgr.AddNamespace("t", "http://schemas.microsoft.com/exchange/services/2006/types");
			this.mgr.AddNamespace("m", "http://schemas.microsoft.com/exchange/services/2006/messages");
			this.ser = new XmlSerializer(typeof(SendNotificationResponseType));
		}

		/// <summary>
		/// Start Listening
		/// </summary>
		public void StartListening()
		{
			VerifyNotListening();
			this.stopEvent.Reset();
			this.shouldStop = false;
			ThreadPool.QueueUserWorkItem(new WaitCallback(ListenOnThread));
		}

		/// <summary>
		/// Stop Listening
		/// </summary>
		public void StopListening()
		{
			VerifyListening();
			this.stopEvent.Set();
		}

		/// <summary>
		/// Thread pool method to start listening on the background thread
		/// </summary>
		/// <param name="state">State - ignore</param>
		/// 
		private void ListenOnThread(object state)
		{
			using (HttpListener listener = new HttpListener())
			{
				listener.Prefixes.Add(String.Format("http://+:{0}/PushNotificationsClient/", this.portNumber.ToString()));
				listener.Start();
				this.isListening = true;
				while (!shouldStop)
				{
					IAsyncResult asyncResult = listener.BeginGetContext(AsyncCallbackMethod, listener);

					// Wait on either the listener or the stop event
					//
					int index = WaitHandle.WaitAny(new WaitHandle[] { stopEvent, asyncResult.AsyncWaitHandle });
					switch (index)
					{
						case 0:
							// Stop event was triggered.
							//
							shouldStop = true;
							break;

						case 1:
							// Notification was received. Just loop around so we can call BeginGetContext again
							//
							break;
					}
				}
				listener.Stop();
			}
			this.isListening = false;
		}

		/// <summary>
		/// Async method called once we receive a request
		/// </summary>
		/// <param name="result">Async result containing our HttpListener</param>
		/// 
		private void AsyncCallbackMethod(IAsyncResult result)
		{
			HttpListener listener = result.AsyncState as HttpListener;
			if (!this.isListening) 
			{
				// Our callback gets fire when we stop the listener too.  If it is not listening,
				// just return.
				//
				return;
			}
			HttpListenerContext context = listener.EndGetContext(result);

			SendNotificationResponseType request;

			// Now use the XML serializer to turn the XML into a notification serialization type...
			//
			XmlDocument doc = new XmlDocument();
			try
			{
				doc.LoadXml(new StreamReader(context.Request.InputStream).ReadToEnd());

				XmlNodeList nodes = doc.SelectNodes("//m:SendNotification[1]", mgr);
				if (nodes.Count == 0)
				{
					// this wasn't a SendNotification request or it was malformed or something like that.
					//
					FailRequest(context);
					return;
				}

				string sendNotification = nodes[0].OuterXml;
				using (MemoryStream ms = new MemoryStream())
				{
					byte[] bytes = Encoding.UTF8.GetBytes(sendNotification);
					ms.Write(bytes, 0, bytes.Length);
					ms.Flush();
					ms.Position = 0L;
					request = (SendNotificationResponseType)this.ser.Deserialize(ms);
				}
			}
			catch (XmlException)
			{
				FailRequest(context);
				return;
			}

			// Fire the delegate
			//
			NotificationResponse response = eventHandler(
								this, 
								request.ResponseMessages.Items[0]
									as SendNotificationResponseMessageType);
			GenerateResponseXML(context, response);
		}

		/// <summary>
		/// Fail the request. Right now we don't differentiate between reasons why it failed.
		/// </summary>
		/// <param name="context">Request context</param>
		/// 
		private void FailRequest(HttpListenerContext context)
		{
			context.Response.ContentEncoding = Encoding.UTF8;
			context.Response.ContentType = "text/xml; charset=utf-8";
			context.Response.ProtocolVersion = new Version(1, 1, 0, 0);
			context.Response.StatusCode = 400;
			string response = "<?xml version=\"1.0\"?>" +
							  "<Error>Bad Request</Error>";
			byte[] responseBytes = Encoding.UTF8.GetBytes(response);
			context.Response.ContentLength64 = responseBytes.Length;
			context.Response.OutputStream.Write(
				responseBytes, 0, responseBytes.Length);
			context.Response.OutputStream.Flush();
		}

		/// <summary>
		/// Generate the response xml
		/// </summary>
		/// <param name="context">call context</param>
		/// <param name="response">The response enum value</param>
		/// 
		private void GenerateResponseXML(HttpListenerContext context, NotificationResponse response)
		{
			StringBuilder builder = new StringBuilder();
			builder.AppendLine("<?xml version=\"1.0\"?>");
			builder.AppendLine("<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">");
			builder.AppendLine("<s:Body>");
			builder.AppendLine("	<SendNotificationResult xmlns=\"http://schemas.microsoft.com/exchange/services/2006/messages\">");
			builder.AppendFormat("		<SubscriptionStatus>{0}</SubscriptionStatus>\r\n", response.ToString());
			builder.AppendLine("	</SendNotificationResult>");
			builder.AppendLine("</s:Body>");
			builder.AppendLine("</s:Envelope>");

			context.Response.ContentEncoding = Encoding.UTF8;
			context.Response.ContentType = "text/xml; charset=utf-8";
			context.Response.ProtocolVersion = new Version(1, 1, 0, 0);
			context.Response.StatusCode = 200;
			byte[] responseBytes = Encoding.UTF8.GetBytes(builder.ToString());
			context.Response.ContentLength64 = responseBytes.Length;
			context.Response.OutputStream.Write(
				responseBytes, 0, responseBytes.Length);
			context.Response.OutputStream.Flush();
		}

		/// <summary>
		/// Returns true if the listener is listening
		/// </summary>
		public bool IsListening
		{
			get
			{
				return isListening;
			}
		}

		/// <summary>
		/// Verifies that the listener isn't listening
		/// </summary>
		private void VerifyNotListening()
		{
			if (isListening)
			{
				throw new PushNotificationStateException("Cannot perform this operation when listening");
			}
		}

		/// <summary>
		/// Verifies that the listener is listening
		/// </summary>
		private void VerifyListening()
		{
			if (!isListening)
			{
				throw new PushNotificationStateException("Cannot perform this operation when not listening");
			}
		}
	}
}
